package game.games.gol;
import game.Abstract2DMap;
import game.AbstractGame;
import game.Point;
import game.Util;
import game.console.Console;
import game.console.IConsoleCommand;
import game.coord.MapGenerator;

import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import javax.swing.SwingUtilities;


public class GameOfLife extends Abstract2DMap
{
    private static GameOfLife instance;
    
    public synchronized static GameOfLife getInstance()
    {
        if (instance == null)
        {
            instance = new GameOfLife();
            synchronized (instance)
            {
                for (IConsoleCommand commandObj : GameOfLifeCommands.getCommands())
                {
                    Console.getInstance().addConsoleCommand(commandObj);
                }
            }
        }
        return instance;
    }
    
    private List<Cell>      cells                = new ArrayList<Cell>();
    
    private int             generation           = 0;
    private int             livingCells          = 0;
    private int             livingCellsIdentical = 0;
    private int             nextLivingCells      = 1500;
    
    private GameOfLifeRule rule                 = GameOfLifeRule.DEFAULT;
    
    private ColorSet        colorSet             = ColorSet.DEFAULT;
    private static boolean  CREATE_LIVE_MODE     = false;
    
    public void setRule(String rule)
    {
        GameOfLifeRule newRule = new GameOfLifeRule(rule);
        setRule(newRule);
    }
    
    public void setRule(GameOfLifeRule newRule)
    {
        try
        {
            pause();
            this.rule = newRule;
            resume();
        }
        catch(Exception ex)
        {
            AbstractGame.log(ex);
        }
    }
    
    private Cell getCell(Point position)
    {
        int index = calculateIndex(position.getX(), position.getY(), getSize());
        return cells.get(index);
    }
    
    public void setLivingCells(int livingCells)
    {
        if (livingCells > cells.size())
        {
            livingCells = cells.size();
        }
        this.nextLivingCells = livingCells;
    }
    
    public void setProzentLivingCells(int prozent)
    {
        if (prozent >= 100)
        {
            nextLivingCells = cells.size();
        }
        else
        {
            nextLivingCells = (cells.size() / 100) * prozent;
        }
    }
    
    private final int maxThreads = 8;
    
    private class RandomLivingCellThread extends Thread
    {
        private List<Point> livingCells = new ArrayList<Point>();
        
        public RandomLivingCellThread(String string)
        {
            super(string);
        }

        public List<Point> getLivingCells()
        {
            return livingCells;
        }
        
        @Override
        public void run()
        {
            // Lebende Zellen generieren
            Random RANDOM = new Random(System.nanoTime());
            do
            {
                Point position = calculatePosition(RANDOM.nextInt(getSize().getSum()), getSize());
                if (position.getX() > 0 && position.getY() > 0 && livingCells.contains(position) == false)
                {
                    livingCells.add(position);
                }
            }
            while (livingCells.size() < nextLivingCells / maxThreads);
        }
    }
    
    private static boolean AddLivingCellsThreadIsRunning = false;
    private static Thread  AddLivingCellsThreadInstance  = null;
    
    private class AddLivingCellsThread implements Runnable
    {
        
        @Override
        public void run()
        {
            try
            {
                long startTime = System.nanoTime();
                if (isRunning() && isPaused() == false)
                {
                    pause();
                }
                final List<Point> livingCells = Collections.synchronizedList(new ArrayList<Point>());
                do
                {
                    List<RandomLivingCellThread> threads = new ArrayList<RandomLivingCellThread>();
                    while(threads.size() < maxThreads)
                    {
                        // return
                        if (AddLivingCellsThreadIsRunning == false)
                        {
                            throw new InterruptedException();
                        }
                        RandomLivingCellThread t = new RandomLivingCellThread("RandomLivingCellsThread " + threads.size());
                        t.start();
                        threads.add(t);
                    }
                    for (RandomLivingCellThread thread : threads)
                    {
                        // return
                        if (AddLivingCellsThreadIsRunning == false)
                        {
                            throw new InterruptedException();
                        }
                        while (thread.isAlive())
                        {
                         // return
                            if (AddLivingCellsThreadIsRunning == false)
                            {
                                throw new InterruptedException();
                            }
                            try
                            {
                                thread.join();
                            }
                            catch (InterruptedException e)
                            {
                                //
                            }
                        }
                        for (Point p : thread.getLivingCells())
                        {
                         // return
                            if (AddLivingCellsThreadIsRunning == false)
                            {
                                throw new InterruptedException();
                            }
                            if (livingCells.contains(p) == false)
                            {
                                livingCells.add(p);
                            }
                        }
                    }
                }
                while(livingCells.size() < nextLivingCells);
                while (livingCells.size() > nextLivingCells)
                {
                    // return
                    if (AddLivingCellsThreadIsRunning == false)
                    {
                        throw new InterruptedException();
                    }
                    livingCells.remove(0);
                }
                // reset count
                for (Point position : livingCells)
                {
                    // return
                    if (AddLivingCellsThreadIsRunning == false)
                    {
                        throw new InterruptedException();
                    }
                    Cell cell = getCell(position);
                    cell.setAlive(true);
                    cell.setSurviving(true);
                }
                AbstractGame.log(livingCells.size() + " living cells added (" + Util.nanoToSec(System.nanoTime() - startTime) + " sec)");
                if (isRunning() && isPaused())
                {
                    resume();
                }
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            finally
            {
                AddLivingCellsThreadIsRunning = false;
            }
        }
    }
    
    private void addLivingCells()
    {
        if (AddLivingCellsThreadInstance == null)
        {
            AddLivingCellsThreadInstance = new Thread(new AddLivingCellsThread(), "AddLivingCellsThread");
            AddLivingCellsThreadIsRunning = true;
            AddLivingCellsThreadInstance.start();
        }
        else
        {
            AddLivingCellsThreadIsRunning = false;
            while (AddLivingCellsThreadInstance.isAlive())
            {
                try
                {
                    AddLivingCellsThreadInstance.join();
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
            AddLivingCellsThreadInstance = null;
        }
    }
    
    private void resetGeneration()
    {
        pause();
        long startTime = System.nanoTime();
        for (Iterator<Cell> i = cells.iterator(); i.hasNext();)
        {
            Cell cell = i.next();
            cell.setAlive(false);
            cell.setSurviving(false);
        }
        generation = 0;
        AbstractGame.log("resetGeneration(" + cells.size() + ") (" +  + Util.nanoToSec(System.nanoTime() - startTime)  + " sec)");
    }
    
    @Override
    protected void restart()
    {
        resetGeneration();
        Point newSize = calcMaxSize(new Point(GameOfLifeRunner.getInstance().getSize()));
        if (newSize.getSum() != cells.size())
        {
            initGeneration(newSize);
        }
        else
        {
            addLivingCells();
            start();
        }
    }
    
    private void initGeneration(Point xy)
    {
        // Anzahl Zellen
        long startTime = System.nanoTime();
        Point size = new Point(xy.getX(), xy.getY());
        setSize(size);
        // Liste generieren
        if (cells.isEmpty())
        {
            for (Cell cell : MapGenerator.generateMooreMap(size, Cell.class))
            {
                cells.add(cell);
            }
        }
        else
        {
            int count = size.getSum();
            while (cells.size() > count)
            {
                cells.remove(0);
            }
            while (cells.size() < count)
            {
                cells.add(new Cell());
            }
            MapGenerator.rebuildMooreMap(size, cells);
        }
        AbstractGame.log("initGeneration(" + xy.getX() + "/" + xy.getY() + ") complete, " + cells.size() + " cells created (" +  + Util.nanoToSec(System.nanoTime() - startTime)  + " sec)");
        addLivingCells();
        generation = 0;
        start();
    }
    
    public void setColorSet(ColorSet colorSet)
    {
        this.colorSet = colorSet;
    }
    
    public ColorSet getColorSet()
    {
        return colorSet;
    }
    
    @Override
    public void render(Graphics g)
    {
        if (isRunning())
        {
            for (int i = 0; i < cells.size(); i++)
            {
                Cell cell = cells.get(i);
                g.setColor(colorSet.getColor(cell.isAlive(), cell.isSurviving()));
                Point position = calculatePosition(i, getSize());
                g.fillRect((int) position.getWidth() * cellSize, (int) position.getHeight() * cellSize, cellSize, cellSize);
                if (isRunning() && isPaused() == false)
                {
                    // next round
                    cell.setAlive(cell.isSurviving());
                }
            }
        }
    }
    
    @Override
    public void doNext()
    {
        generation++;
        int alive = 0;
        for (int i = 0; i < cells.size(); i++)
        {
            Cell cell = cells.get(i);
            int livingNeighbors = cell.countLivingNeighbors();
            cell.setSurviving(rule.isSurviving(cell.isAlive(), livingNeighbors));
            if (cell.isAlive())
            {
                alive++;
            }
        }
        if (alive == livingCells)
        {
            livingCellsIdentical++;
        }
        else
        {
            livingCellsIdentical = 0;
        }
        livingCells = alive;
        setStatus("Generation " + generation + "; " + livingCells + " Living Cells");
        if (livingCells <= 0)
        {
            AbstractGame.log("No more living cells");
            requestRestart();
        }
        else if (livingCellsIdentical > 50)
        {
            requestRestart();
        }
    }

    @Override
    public KeyListener getKeyboardControl()
    {
        final KeyListener xsuper = super.getKeyboardControl();
        return new KeyAdapter()
        {
            private boolean IGNORE_KEYBOARD       = false;
            
            @Override
            public void keyPressed(KeyEvent e)
            {
                if (IGNORE_KEYBOARD)
                {
                    e.consume();
                }
                else
                {
                    IGNORE_KEYBOARD = true;
                    final Console console = Console.getInstance();
                    if (console.isOpen() == false)
                    {
                        switch (e.getKeyCode())
                        {
                            case KeyEvent.VK_P:
                            {
                                if (instance.isRunning())
                                {
                                    if (instance.isPaused())
                                    {
                                        instance.resume();
                                    }
                                    else
                                    {
                                        instance.pause();
                                    }
                                }
                                break;
                            }
                            case KeyEvent.VK_S:
                            {
                                instance.requestStop();
                                break;
                            }
                            case KeyEvent.VK_R:
                            {
                                instance.requestRestart();
                                break;
                            }
                            default:
                            {
                                xsuper.keyPressed(e);
                                break;
                            }
                        }
                    }
                    IGNORE_KEYBOARD = false;
                }
            }
        };
    }

    @Override
    public MouseListener getMouseControl()
    {
        return new MouseAdapter()
        {
            @Override
            public void mouseClicked(MouseEvent e)
            {
                if (SwingUtilities.isRightMouseButton(e))
                {
                    CREATE_LIVE_MODE = CREATE_LIVE_MODE == false;
                    AbstractGame.log("CREATIVE_LIVE_MODE " + CREATE_LIVE_MODE);
                }
            }
        };
    }
    
    @Override
    public MouseMotionListener getMouseMotionListener()
    {
        return new MouseMotionAdapter() 
        {
            @Override
            public void mouseMoved(MouseEvent e)
            {
                if (CREATE_LIVE_MODE)
                {
                    try
                    {
                        Point pos = calcCoord(e.getPoint());
                        AbstractGame.log("SURVIVING: " + pos);
                        Cell cell = getCell(pos);
                        cell.setAlive(true);
                        cell.setSurviving(true);
                    }
                    catch(IllegalArgumentException ex)
                    {
                        ex.printStackTrace();
                    }
                }
            }
        };
    }

    public static class GameOfLifeRunner extends AbstractGame
    {
        private static GameOfLifeRunner instance;
        
        public synchronized static GameOfLifeRunner getInstance()
        {
            if (instance == null)
            {
                instance = new GameOfLifeRunner();
            }
            return instance;
        }
        
        protected GameOfLifeRunner()
        {
            super(GameOfLife.getInstance());
        }
        
        public static void main(String[] args)
        {
             Thread t = new Thread(GameOfLifeRunner.getInstance(), "GameOfLife");
             t.start();
        }
    }
}
